今天先建立起簡單的視訊會議做驗證。
建立視訊會議的方式,基本上就是昨天描述的過程的實作。連線資訊的傳遞,則透過之前包裝好的ws.io模組。
首先看一下伺服器:
var app = require('http').createServer(handler),
io = require('./ws.io').listen(app),
fs = require('fs'),
url = require('url');
app.listen(8443);
function handler (req, res) {
var filename = '';
var resource = url.parse(req.url).pathname;
switch(resource) {
case '/ws.io/ws.io.js':
res.setHeader('Content-Type', 'text/javascript');
filename = __dirname + resource;
break;
default:
res.setHeader('Content-Type', 'text/html');
filename = __dirname + '/test847.html';
break
}
fs.readFile(filename, function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading index.html');
}
res.writeHead(200);
res.end(data);
});
}
io.sockets.on('connection', function (socket) {
socket.on('offer', function(data) {
socket.broadcast.emit('offer', {sdp: data.sdp});
});
socket.on('answer', function(data) {
socket.broadcast.emit('answer', {sdp: data.sdp});
});
socket.on('ice', function(data) {
socket.broadcast.emit('ice', {sdp:data.sdp,label:data.label});
});
socket.on('startice', function(data) {
socket.broadcast.emit('startice', {});
});
socket.on('hangup', function() {
socket.broadcast.emit('hangup', {});
});
});
簡單地說,其實就是處理在caller產生offer(Session Description)傳送,callee產生answer(Session Description)回傳,傳送ICE,通知啟動ICE,通知停止通訊等事件。
再來看一下瀏覽器端:
<style>
video {
border: solid 1px #6699cc;
border-radius: 10px;
padding: 15px 15px 15px 15px;
}
</style>
<script src='http://code.jquery.com/jquery-1.8.2.js'></script>
<script src='/ws.io/ws.io.js'></script>
<script>
var socket = io.connect('ws://60.248.166.82:8443');
$(document).ready(function() {
var localStream,remoteStream;
var pc = null;
$('#btnStart').click(function() {
navigator.webkitGetUserMedia({video:true,audio:true}, function(stream) {
localStream = stream;
$('#local').attr('src', webkitURL.createObjectURL(stream));
}, function(info) {
console.log('getUserMedia Error:' + info);
});
this.disabled = true;
$('#btnCall').attr('disabled', false);
});
$('#btnCall').click(function() {
this.disable = true;
$('#btnHangup').attr('disabled', false);
//pc = new webkitPeerConnection00(null, function(candidate, more) {
pc = new webkitPeerConnection00('STUN stun.1.google.com:19302', function(candidate, more) {
if(candidate) {
console.log(candidate);
socket.emit('ice', {target: 'notme',sdp:candidate.toSdp(),label:candidate.label});
}
});
pc.onaddstream = function(e) {
remoteStream = e.stream;
$('#remote').attr('src', webkitURL.createObjectURL(e.stream));
$('#btnHangup').attr('disabled', false);
};
pc.addStream(localStream);
var offer = pc.createOffer(null);
pc.setLocalDescription(pc.SDP_OFFER, offer);
socket.emit('offer', {target:'notme',sdp:offer.toSdp()});
});
$('#btnHangup').click(function() {
this.disable = true;
pc.close();
pc = null;
socket.emit('hangup',{});
$(this).attr('disabled', true);
$('#btnCall').attr('disabled', false);
});
socket.on('ice', function(data) {
pc.processIceMessage(new IceCandidate(data.label,data.sdp));
console.log('socket on ice: ', getIceStateDesc(pc.iceState));
});
socket.on('offer', function(data) {
pc = new webkitPeerConnection00(null, function(candidate, more) {
if(candidate) {
console.log(candidate);
socket.emit('ice', {target:'notme',sdp:candidate.toSdp(),label:candidate.label});
}
});
pc.onaddstream = function(e) {
remoteStream = e.stream;
$('#remote').attr('src', webkitURL.createObjectURL(e.stream));
$('#btnHangup').attr('disabled', false);
};
pc.addStream(localStream);
pc.setRemoteDescription(pc.SDP_OFFER, new SessionDescription(data.sdp));
var answer = pc.createAnswer(data.sdp, {has_video:true,has_audio:true});
pc.setLocalDescription(pc.SDP_ANSWER, answer);
socket.emit('answer', {target:'notme',sdp:answer.toSdp()});
});
socket.on('answer', function(data) {
pc.setRemoteDescription(pc.SDP_ANSWER, new SessionDescription(data.sdp));
socket.emit('startice', {target:'notme'});
pc.startIce();
});
socket.on('startice', function() {
pc.startIce();
});
socket.on('hangup', function() {
$('#btnHangup').attr('disabled', true);
$('#btnCall').attr('disabled', false);
pc.close();
pc = null;
});
function getIceStateDesc(state) {
switch(state) {
case 0x100:
return 'ICE_GATHERING';
break;
case 0x200:
return 'ICE_WAITING';
break;
case 0x300:
return 'ICE_CHECKING';
break;
case 0x400:
return 'ICE_CONNECTED';
break;
case 0x500:
return 'ICE_COMPLETED';
break;
case 0x600:
return 'ICE_FAILED';
break;
case 0x700:
return 'ICE_CLOSED';
break;
default:
return '';
}
}
});
</script>
<div>
<legend>Start a video conference</legend>
<video id="local" width="320" autoplay></video>
<video id="remote" width="320" autoplay></video><br>
<button class="btn" id="btnStart">start</button>
<button class="btn" id="btnCall" disabled>call</button>
<button class="btn" id="btnHangup" disabled>hangup</button>
</div>
過程跟做天描述的一樣。用畫面來看比較清楚:
剛開啟網頁時:
按下start,透過getUserMedia要求取得視訊資源,瀏覽器會詢問使用者是否要接受:
接受以後,就會看到本地的視訊:
遠端也要先start。接下來按下call,開始PeerConnection的流程,雙方建立連線後,對方的視訊就開始播放:
按下hangup按鈕,可以中斷網路連接:
再按下call又可以建立連接,重新收到視訊:
不過目前這樣寫,其實有三個人的時候,就會出問題。要解決的話,就需要每兩人之間都建立一組PeerConnection連線,這個就等到明天再來嘗試,不過不保証成功(我在WebRTC maillist上面看到很多問題,說真的,目前的實作只是可用而已)
(2012-11-13 3:52)
會後補充
Chrome23把PeerConnection的介面又大改了一次,雖然這樣改跟W3C的規格又接近了一點,不過之前寫的程式在Chrome23之後就不能動了
Chrome23的實作基本上是依照:http://www.w3.org/TR/webrtc/ (日期應該是2012-8-21,後續如果有正式的draft,日期可能會變動,不過Chrome23的實作是依照8/23這個版本的規格)
首先必須注意,在http://www.webrtc.org/faq-recent-topics有提到,目前的實作與W3C還是稍有不同,不依照他的「不同」寫法,會出現...DOM Exception。
把今天的範例調整過,伺服器(test851.js):
var app = require('http').createServer(handler),
io = require('./ws.io').listen(app),
fs = require('fs'),
url = require('url');
app.listen(8443);
function handler (req, res) {
var filename = '';
var resource = url.parse(req.url).pathname;
switch(resource) {
case '/ws.io/ws.io.js':
res.setHeader('Content-Type', 'text/javascript');
filename = __dirname + resource;
break;
case '/js/jquery-1.8.2.js':
res.setHeader('Content-Type', 'text/javascript');
filename = __dirname + resource;
break;
default:
res.setHeader('Content-Type', 'text/html');
filename = __dirname + '/test851.html';
break
}
fs.readFile(filename, function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading index.html');
}
res.writeHead(200);
res.end(data);
});
}
io.sockets.on('connection', function (socket) {
socket.on('offer', function(data) {
console.log('offer', data);
socket.broadcast.emit('offer', {sdp: data.sdp});
});
socket.on('answer', function(data) {
console.log('answer', data);
socket.broadcast.emit('answer', {sdp: data.sdp});
});
socket.on('ice', function(data) {
socket.broadcast.emit('ice', {candidate: data.candidate});
});
socket.on('startice', function(data) {
socket.broadcast.emit('startice', {});
});
socket.on('hangup', function() {
socket.broadcast.emit('hangup', {});
});
});
主要的修改還是在client端(test851.html):
<style>
video {
border: solid 1px #6699cc;
border-radius: 10px;
padding: 15px 15px 15px 15px;
}
</style>
<script src='js/jquery-1.8.2.js'></script>
<script src='/ws.io/ws.io.js'></script>
<script>
var socket = io.connect('ws://localhost:8443');
var localStream,remoteStream;
var pc = null;
$(document).ready(function() {
$('#btnStart').click(function() {
navigator.webkitGetUserMedia({video:true,audio:true}, function(stream) {
localStream = stream;
$('#local').attr('src', webkitURL.createObjectURL(stream));
}, function(info) {
console.log('getUserMedia Error:' + info);
});
this.disabled = true;
$('#btnCall').attr('disabled', false);
});
$('#btnCall').click(function() {
this.disable = true;
$('#btnHangup').attr('disabled', false);
pc = new webkitRTCPeerConnection(null);
pc.onaddstream = function(e) {
remoteStream = e.stream;
$('#remote').attr('src', webkitURL.createObjectURL(e.stream));
$('#btnHangup').attr('disabled', false);
};
pc.onicecandidate = function(e) {
if(!!e.candidate) {
socket.emit('ice', {candidate: e.candidate});
}
};
pc.addStream(localStream);
pc.createOffer(function(sdp) {
console.log('createOffer', sdp);
pc.setLocalDescription(sdp, function() {
console.log('after setLocalDescription');
socket.emit('offer', {sdp:sdp});
});
}, function() {
console.log('create offer error.');
},{has_video:true,has_audio:true});
});
$('#btnHangup').click(function() {
this.disable = true;
pc.close();
pc = null;
socket.emit('hangup',{});
$(this).attr('disabled', true);
$('#btnCall').attr('disabled', false);
});
socket.on('ice', function(data) {
console.log(data.candidate);
console.log('socket on ice: ', getIceStateDesc(pc.iceState));
pc.addIceCandidate(new RTCIceCandidate(data.candidate));
});
socket.on('offer', function(data) {
pc = new webkitRTCPeerConnection(null);
pc.onaddstream = function(e) {
remoteStream = e.stream;
$('#remote').attr('src', webkitURL.createObjectURL(e.stream));
$('#btnHangup').attr('disabled', false);
};
pc.onicecandidate = function(e) {
if(!!e.candidate) {
socket.emit('ice', {candidate: e.candidate});
}
};
pc.addStream(localStream);
var offer = new RTCSessionDescription(data.sdp);
pc.setRemoteDescription(offer, function() {
//pc.createAnswer is different from w3c webrtc standard api
console.log('set remote description');
pc.createAnswer(function(sdp) {
console.log('createAnswer', sdp);
pc.setLocalDescription(sdp, function() {
console.log('set local description');
socket.emit('answer', {sdp:sdp});
}, function(e) {
console.log('set local description error: '+e);
});
}, function(e) {
console.log('create answer error: '+e);
}, {has_video:true,has_audio:true});
}, function(e) {
console.log('set remote description error: '+e);
});
});
socket.on('answer', function(data) {
console.log('on answer triggered');
pc.setRemoteDescription(new RTCSessionDescription(data.sdp));
});
socket.on('hangup', function() {
$('#btnHangup').attr('disabled', true);
$('#btnCall').attr('disabled', false);
pc.close();
pc = null;
});
function getIceStateDesc(state) {
switch(state) {
case 0x100:
return 'ICE_GATHERING';
break;
case 0x200:
return 'ICE_WAITING';
break;
case 0x300:
return 'ICE_CHECKING';
break;
case 0x400:
return 'ICE_CONNECTED';
break;
case 0x500:
return 'ICE_COMPLETED';
break;
case 0x600:
return 'ICE_FAILED';
break;
case 0x700:
return 'ICE_CLOSED';
break;
default:
return '';
}
}
});
</script>
<div>
<legend>Start a video conference</legend>
<video id="local" width="320" autoplay></video>
<video id="remote" width="320" autoplay></video><br>
<button class="btn" id="btnStart">start</button>
<button class="btn" id="btnCall" disabled>call</button>
<button class="btn" id="btnHangup" disabled>hangup</button>
</div>
https://gist.github.com/0bc90e7df06ea841acf8
這個是只能測試兩個peer的,多了會有問題。不過建議把ws.io換成socket.io,比較不會碰到問題。(瀏覽器要用的ws.io.js檔案,還沒有像socket.io那樣處理,會比較不方便)
另外,這是用Chrome23才支援的API寫的,所以必須使用Chrome23才會動。
要到Chrome24才能支援TURN協定,目前使用預設的STUN協定,當兩個Peer都在NAT後面時穩死。
感謝費公總是仁民愛物、大公無私!
「當兩個Peer都在NAT後面時穩死 」是指 firewall 嗎? 還是只要是兩個不同的 IP 分享器的內網,就不能用了呢?
事隔三年看到這篇真是好久台灣都沒有出這類WebRTC的書~
我到處找視訊聊天室需求~
找到大陸公開的版本使用~他是一對多的~目前使用還ok~
供您參考~哈^^"
http://segmentfault.com/a/1190000000439103
https://github.com/LingyuCoder/SkyRTC
可惜..IE跟蘋果手機不支援WebRTC的視訊呢...
「支援」還是最大的問題阿...另一個問題是成本,雖然號稱直接連線,不過實際上大部分使用者都是在NAT後面,需要透過一台TURN伺服器來relay,這樣視訊的頻寬成本就不小了...
他這個聊天室的版本是借用Google的TURN伺服器stun:stun.l.google.com:19302
如果要安裝本地TURN伺服器,就要另外下在TURN安裝套件了~
頻寬還在實驗中~如果100MB不夠使用~就多申請幾條線路來分流@@"
雖然是一對多~但目前人數都是要預約才能使用的~不是像外面的一般的聊天室可以隨時進來。
基本上我都會要求使用者必須是Google瀏覽器才可登入,其他瀏覽器不可登入。(用JQ擋登入)
我發現蘋果手機下載這套Bowser WebRTC
http://www.openwebrtc.io/bowser/
他的介紹~我在蘋果手機試他的範例是可以看的到視訊~
但是試大陸版本那套是開啟失敗的~不知道原因差異在哪裡@@"
您好,最近在研究webrtc方面的問題,看了您的文章以及使用您的範例,發現無法使用
請問是什麼問題嗎?
因為過了七年,有些東西可能不一樣了XD
我要找時間來看看...
createObjectURL 已經失效了(被封鎖)
WebRTC改新的讀取影像方式了...
https://ithelp.ithome.com.tw/articles/10210809